using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using Bamboo.Utils;

namespace Bamboo.ExeShell
{
    /// <summary>
    /// 长连接封装的PLINK
    /// 这个类解决了一些奇怪的环境问题：
    /// 1. plink都退出了，命令执行也成功了，但是收到的输出信息不完整。现在的策略是如果进程结束了，再多等5秒，收到结束标志再停止。
    /// 2. 命令执行成功了，plink一直不退出。现在的策略是收到结束标志直接kill进程。
    /// 使用本类要注意：
    /// 1. 不一定能接收到plink的退出代码了，因为有时候收到结束标志就直接kill进程了。
    /// </summary>
    public class PlinkExec
    {
        /// <summary>
        /// 填写plink.exe完整路径，如果留空，初始化连接时会从当前目录和Path环境变量搜索
        /// </summary>
        public string PlinkFileName { get; set; }
        /// <summary>
        /// 主机名或IP地址
        /// </summary>
        public string HostName { get; }
        /// <summary>
        /// 端口号，默认22
        /// </summary>
        public int Port { get; }
        /// <summary>
        /// 用户名
        /// </summary>
        public string UserName { get; }
        /// <summary>
        /// 密码
        /// </summary>
        public string Password { get; }
        /// <summary>
        /// 命令行字符集，默认UTF-8
        /// </summary>
        public Encoding CmdEncoding { get; }

        /// <summary>
        /// 所有输出流回调（由异步线程发起调用）
        /// </summary>
        public event Action<string> StdAllHandler;
        /// <summary>
        /// 标准输出流回调（由异步线程发起调用）
        /// </summary>
        public event Action<string> StdOutHandler;
        /// <summary>
        /// 标准错误流回调（由异步线程发起调用）
        /// </summary>
        public event Action<string> StdErrorHandler;

        /// <summary>
        /// 调试日志回调（由异步线程发起调用）
        /// </summary>
        public event Action<string> DebugLogHandler;

        /// <summary>
        /// 调试默认，默认false
        /// </summary>
        public bool DebugMode = false;

        /// <summary>
        /// 标准+错误输出流内容
        /// </summary>
        public List<string> StdAllLines { get; } = new List<string>();
        /// <summary>
        /// 标准输出流内容
        /// </summary>
        public List<string> StdOutLines { get; } = new List<string>();
        /// <summary>
        /// 标准
        /// 错误流内容
        /// </summary>
        public List<string> StdErrorLines { get; } = new List<string>();

        /// <summary>
        /// 是否检测到SSH指纹询问
        /// </summary>
        private bool _foundSshQuestion = false;
        /// <summary>
        /// 匹配到的结束标志下表。-1代表未匹配。
        /// </summary>
        private int _foundExitFlagIndex = -1;
        /// <summary>
        /// 结束标志
        /// </summary>
        public List<string> ExitFlags = new List<string>();

        private System.Threading.AutoResetEvent _foundFlagEvent = new System.Threading.AutoResetEvent(false);

        public int ExitCode { get; private set; } = -9999;
        /// <summary>
        /// 命令行执行的超时时间，默认60秒
        /// </summary>
        public int TimeoutSeconds { get; set; } = 60;
        /// <summary>
        /// 脚本执行结束后，通过本方法查看第一个找到的结束标志。
        /// 如果没找到结束标志，返回空字符串
        /// </summary>
        /// <returns></returns>
        public string GetMatchedExitFlag()
        {
            if (_foundExitFlagIndex >= 0) return ExitFlags[_foundExitFlagIndex];
            else return "";
        }

        public PlinkExec(string hostName, int port, string userName, string password, string encodingName)
        {
            this.HostName = hostName;
            this.Port = port;
            this.UserName = userName;
            this.Password = password;
            this.CmdEncoding = Encoding.GetEncoding(encodingName);
        }

        public void ClearOutputBuffer()
        {
            StdAllLines.Clear();
            StdOutLines.Clear();
            StdErrorLines.Clear();
        }

        public string LocatePlinkExe()
        {
            var cmd = new ProcessExecute();
            cmd.FileName = "where.exe";
            cmd.Arguments = "plink.exe";
            cmd.IgnoreError = false;
            cmd.DebugMode = false;
            cmd.Run();
            var output = cmd.OutputBuffer.ToString();
            var lines = output.Split('\r', '\n');
            foreach (var line in lines)
            {
                if (string.IsNullOrWhiteSpace(line)) continue;
                if (System.IO.File.Exists(line)) return line;
            }
            throw new Exception("未找到plink.exe具体位置，请检查PATH环境变量配置");
        }

        private void DetectFlag(string s)
        {
            if (string.IsNullOrWhiteSpace(s)) return;
            //检测SSH指纹询问
            if (!_foundSshQuestion && StdAllLines.Count < 100 && (s.Contains("The server's host key") || s.Contains("changed the host key")))
            {
                LogDebug("检测到SSH指纹询问" + s.Trim());
                _foundSshQuestion = true;
                _foundFlagEvent.Set();
            }
            //检测退出标志
            if (ExitFlags.Count > 0 && _foundExitFlagIndex < 0)
            {
                for (int i = 0; i < ExitFlags.Count; ++i)
                {
                    if (s.Contains(ExitFlags[i]))
                    {
                        _foundExitFlagIndex = i;
                        _foundFlagEvent.Set();
                        LogDebug("检测到结束标志：" + ExitFlags[i]);
                        break;
                    }
                }
            }
        }

        private void OnOutputDataReceived(object sender, DataReceivedEventArgs e)
        {
            if (e.Data == null) return;
            lock (StdAllLines)
            {
                DetectFlag(e.Data);
                string s = e.Data.TrimEnd();
                StdOutLines.Add(s);
                StdAllLines.Add(s);
                if (StdOutHandler != null) StdOutHandler(s);
                if (StdAllHandler != null) StdAllHandler(s);
            }

        }

        private void OnErrorDataReceived(object sender, DataReceivedEventArgs e)
        {
            if (e.Data == null) return;
            lock (StdAllLines)
            {
                DetectFlag(e.Data);
                string s = e.Data.TrimEnd();
                StdErrorLines.Add(s);
                StdAllLines.Add(s);
                if (StdErrorHandler != null) StdErrorHandler(s);
                if (StdAllHandler != null) StdAllHandler(s);
            }
        }

        public string GetStdAllText()
        {
            lock (StdAllLines)
            {
                var s = new StringBuilder(16 * 1024);
                foreach (var line in StdAllLines)
                {
                    s.AppendLine(line);
                }
                return s.ToString();
            }

        }

        /// <summary>
        /// 执行SHELL文件
        /// </summary>
        /// <param name="filename"></param>
        /// <param name="exitFlag"></param>
        /// <returns>-1代表进程已退出但没有找到结束标志，0代表找到结束标志</returns>
        public void ExecuteShellFile(string filename)
        {
            InnerExecuteShellFile(filename);
            if (_foundSshQuestion)
            {
                LogDebug("正在应答SSH指纹询问");
                PuttyBase.AutoFixHostKeyError(HostName, Port);
                LogDebug("重新执行一次脚本文件");
                InnerExecuteShellFile(filename);
            }
        }

        public void ExecuteCmd(string cmd)
        {
            if (DebugMode)
            {
                LogDebug("执行SSH命令：" + cmd);
            }
            string tmpFileName = ProcessExecute.GetTempFile("_plinkconnection", ".sh");
            try
            {
                System.IO.File.WriteAllText(tmpFileName, cmd, Encoding.GetEncoding("gbk")); //TODO：这里字符集不是很肯定
                ExecuteShellFile(tmpFileName);
            }
            finally
            {
                try
                {
                    System.IO.File.Delete(tmpFileName);
                }
                catch (Exception)
                {

                }
            }
        }

        /// <summary>
        /// 从全部输出中筛选位于开始标志和结束标志之间的文本
        /// </summary>
        /// <param name="beginFlag"></param>
        /// <param name="endFlag"></param>
        /// <returns></returns>
        public string GetTextByFlag(string beginFlag, string endFlag)
        {
            string s = GetStdAllText();
            int pos1 = s.IndexOf(beginFlag);
            if (pos1 < 0) return "";
            int pos2 = s.IndexOf(endFlag, pos1 + beginFlag.Length);
            if (pos2 < 0) return "";
            string subtext = s.Substring(pos1 + beginFlag.Length, pos2 - pos1 - beginFlag.Length).Trim();
            return subtext;
        }

        private void InnerExecuteShellFile(string filename)
        {
            if (ExitFlags.Count == 0) throw new Exception("请指定至少一个结束标志再执行脚本");
            _foundSshQuestion = false;
            ExitCode = -9999;
            _foundFlagEvent = new System.Threading.AutoResetEvent(false);
            ClearOutputBuffer();

            var si = new ProcessStartInfo();
            si.FileName = string.IsNullOrWhiteSpace(PlinkFileName) ? LocatePlinkExe() : PlinkFileName;
            si.Arguments = $"-batch -ssh -P {Port} -pw {Password} {UserName}@{HostName} -m \"" + filename + "\"";
            si.WindowStyle = ProcessWindowStyle.Hidden;
            si.CreateNoWindow = true;
            si.UseShellExecute = false;
            si.RedirectStandardOutput = true;
            si.RedirectStandardError = true;
            si.RedirectStandardInput = true;
            si.StandardOutputEncoding = CmdEncoding;
            si.StandardErrorEncoding = CmdEncoding;
            using (var p = new Process())
            {
                p.StartInfo = si;
                p.OutputDataReceived += OnOutputDataReceived;
                p.ErrorDataReceived += OnErrorDataReceived;
                p.Exited += OnExited;
               
                p.Start();
                p.BeginOutputReadLine();
                p.BeginErrorReadLine();
                DateTime t1 = DateTime.Now;
                DateTime endTime = t1.AddSeconds(TimeoutSeconds);
                //等待结束标志
                while (_foundExitFlagIndex < 0 && !_foundSshQuestion)
                {
                    _foundFlagEvent.WaitOne(32);
                    DateTime t2 = DateTime.Now;
                    if (t2 >= endTime) throw new Exception("等待结束标志超时");
                    if (p.HasExited)
                    {   //如果进程提前结束了，最多再等5秒，没有结束标志也报错。
                        endTime = t2.AddSeconds(5);
                    }
                }
                if (!p.HasExited)
                {
                    p.Kill();
                }
            }
        }

        private void OnExited(object sender, EventArgs e)
        {
            LogDebug("检测到进程已退出");
            _foundFlagEvent.Set();

        }

        private void LogDebug(string s)
        {
            if (DebugLogHandler == null) return;
            DebugLogHandler(s);
        }

    }
}
